migrate Joi schemas and validation utils to Zod#465
migrate Joi schemas and validation utils to Zod#465CKodidela wants to merge 3 commits intocameri:mainfrom
Conversation
|
@cameri @phoenix-server this PR is up for review |
|
@copilot can you resolve the merge conflicts |
|
@CKodidela I dont think you can trigger. either way doesnt seem to be working |
|
No worries Cameri, i will resolve them on my own |
|
@cameri resolved the merge conflicts, can run checks |
There was a problem hiding this comment.
Pull request overview
This PR migrates the project’s runtime validation from Joi to Zod, updating core NIP-01 schemas and the shared validation utilities so the rest of the codebase can validate/parse inputs via Zod.
Changes:
- Replaced Joi schemas with Zod equivalents for base/event/filter/message schemas (including strict object handling and custom refinements).
- Rewrote
validateSchema/attemptValidationutilities to use Zod parsing. - Updated WebSocket adapter and unit tests to align with Zod error behavior; swapped dependencies from
joitozod.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| test/unit/utils/validation.spec.ts | Updates unit tests for validation helpers to expect ZodError. |
| test/unit/schemas/message-schema.spec.ts | Adjusts message schema tests for Zod-driven validation results. |
| test/unit/schemas/filter-schema.spec.ts | Adjusts filter schema tests for Zod-driven validation results. |
| test/unit/schemas/event-schema.spec.ts | Adjusts event schema tests for Zod-driven validation results. |
| src/utils/validation.ts | Replaces Joi-based validation helpers with Zod-based parsing helpers. |
| src/schemas/message-schema.ts | Migrates message tuple/union validation from Joi conditionals to Zod tuples/unions. |
| src/schemas/filter-schema.ts | Migrates filter schema to Zod with catchall + superRefine for dynamic #[a-z] keys. |
| src/schemas/event-schema.ts | Migrates event schema to z.object(...).strict() to reject unknown keys. |
| src/schemas/base-schema.ts | Migrates core primitives (hex, ints, timestamps, tags) from Joi to Zod. |
| src/adapters/web-socket-adapter.ts | Updates malformed-message handling to recognize Zod validation failures. |
| package.json | Removes Joi and adds Zod dependency. |
| package-lock.json | Lockfile updates reflecting Joi removal and Zod addition. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| it('returns error if filter is missing', () => { | ||
| (message as any[]).splice(2, 2) | ||
|
|
||
| const result = validateSchema(messageSchema)(message) | ||
| expect(result).to.have.nested.property('error.message', '"REQ message" does not contain [filter]') | ||
| expect(result).to.have.property('error').that.is.not.undefined | ||
| }) |
There was a problem hiding this comment.
These assertions were loosened to only check that an error exists, which no longer verifies which validation rule failed (or that the failure is on the expected path). To keep the tests meaningful with Zod, consider asserting against result.error.issues (e.g., expected path/code) rather than dropping specificity entirely.
| cases[prop].forEach(({ transform, message }) => { | ||
| it(`${prop} ${message}`, () => expect( | ||
| validateSchema(filterSchema)(transform(filter)) | ||
| ).to.have.nested.property('error.message', `"${prop}" ${message}`)) | ||
| ).to.have.property('error').that.is.not.undefined) | ||
| }) |
There was a problem hiding this comment.
The test cases loop no longer asserts the expected failure reason for each prop case; it only checks that some error occurred. Consider asserting on error.issues (path/code) for each case so regressions (wrong key failing, wrong rule failing) don't slip through.
| it('returns error if unknown key is provided', () => { | ||
| Object.assign(event, { unknown: 1 }) | ||
|
|
||
| expect(validateSchema(eventSchema)(event)).to.have.nested.property('error.message', '"unknown" is not allowed') | ||
| expect(validateSchema(eventSchema)(event)).to.have.property('error').that.is.not.undefined | ||
| }) |
There was a problem hiding this comment.
These checks were changed from validating the specific error message to only checking that error is present. That makes the test less able to catch schema regressions. Consider asserting on error.issues (e.g., an issue with path: ['unknown'] for unknown-key cases) instead of dropping the assertion detail entirely.
| try { | ||
| return { value: schema.parse(input), error: undefined } | ||
| } catch (error) { | ||
| return { value: undefined, error: error as z.ZodError } | ||
| } |
There was a problem hiding this comment.
validateSchema currently catches all thrown errors from schema.parse and returns them cast as ZodError. This can silently convert unexpected runtime errors (e.g., thrown inside a .refine() callback) into a validation result and potentially cause callers to treat real bugs as user input errors. Prefer schema.safeParse(input) (which guarantees a ZodError on failure), or only handle error instanceof z.ZodError and rethrow anything else.
| try { | |
| return { value: schema.parse(input), error: undefined } | |
| } catch (error) { | |
| return { value: undefined, error: error as z.ZodError } | |
| } | |
| const result = schema.safeParse(input) | |
| if (result.success) { | |
| return { value: result.data, error: undefined } | |
| } | |
| return { value: undefined, error: result.error } |
| } else if (error.name === 'SyntaxError' || error.name === 'ZodError') { | ||
| debug('invalid message client %s (%s): %s', this.clientId, this.getClientAddress(), error.message) | ||
| this.sendMessage(createNoticeMessage(`invalid: ${error.message}`)) |
There was a problem hiding this comment.
The adapter is branching on error.name === 'ZodError'. This is brittle (name can be changed and any error can spoof it). Prefer importing ZodError and checking error instanceof ZodError so only genuine validation failures are handled as malformed client messages.
| debug('invalid message client %s (%s): %s', this.clientId, this.getClientAddress(), error.message) | ||
| this.sendMessage(createNoticeMessage(`invalid: ${error.message}`)) | ||
| } else { |
There was a problem hiding this comment.
createNoticeMessage(invalid: ${error.message}) will send the raw ZodError.message to clients, which is typically a stringified list of issues and can be verbose and hard to read. Consider formatting this down to a stable, single-line summary (e.g., first issue message + path) before sending, to keep notices small and client-friendly.
|
@CKodidela Ci checks failing now, could you please address? |
|
Sure will do it. |
|
@cameri I believe the first three suggestions from copilot arent worth changing, these tests were loosened during the Joi to Zod migration, the tests are not broken they're just less specific. I will fix the rest and run checks locally |
…essage, fix callback schemas and tests
|
@cameri resolved the suggestions, ran checks locally all checks passed. |
|
@CKodidela ci checks are failing |
|
Hmm i guess this takes time let me see the log, checks are passing locally should find out the cause |
|
Got it i didnt make a conventional commit at the beginning, let me close this pr and make a new one |
Description
Replaces the
joivalidation library withzodacross all schemas and validation utilities.Changed files:
src/schemas/base-schema.tshex/lowercase via regex, integer checks via.int(), customcreatedAtvia.refine(),tagSchemausingz.tuple().rest()src/schemas/event-schema.tsz.object().strict()replaces.unknown(false)src/schemas/filter-schema.ts.catchall()+.superRefine()handle dynamic#[a-z]tag filter keyssrc/schemas/message-schema.tsz.union()replacesalternatives().conditional();z.tuple().rest()withsuperRefinefor REQ boundssrc/utils/validation.tsvalidateSchema/attemptValidationrewritten against Zod APIsrc/adapters/web-socket-adapter.tserror name check updated fromValidationErrortoZodErrorRelated Issue
Closes #455
Motivation and Context
joiis a large runtime dependency.zodprovides equivalent validation with a TypeScript-first design, better typeinference, and a smaller footprint. All existing validation behaviour is preserved.
How Has This Been Tested?
All existing unit tests pass (381 passing) after updating schema spec files to work with Zod's error format. The 2
pre-existing failures in
settings.spec.tsare unrelated Windows path separator issues.Types of changes
Checklist: